Виртуальные методы

При раннем связывании программа, готовая для выполнения, представляет собой структуру, логика выполнения которой жестко определена. Если же требуется, чтобы решение о том, какой из одноименных методов разных объектов иерархии использовать, принималось в зависимости от конкретного объекта, для которого выполняется вызов, то заранее жестко связывать эти методы с остальной частью кода нельзя.

Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в C# существует ключевое слово virtual. Оно записывается в заголовке метода базового класса, например:

virtual public void Display()
{...}

Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться в момент его вызова во время выполнения программы. Этот механизм называется поздним связыванием.

Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table, VMT). В нее записываются адреса виртуальных методов (в том числе унаследованных) в порядке описания в классе. Для каждого класса создается одна таблица.

Каждый объект во время выполнения должен иметь доступ к VMT. Связь экземпляра объекта с VMT устанавливается с помощью специального кода, автоматически помещаемого компилятором в конструктор объекта.

Если в производном классе требуется переопределить виртуальный метод, используется ключевое слово override, например:

override public void Display () ...

Переопределенный виртуальный метод должен обладать таким же набором параметров, как и одноименный метод базового класса.

Добавим в листинг 1 лекции «Наследование» два ключевых слова — virtual и override — в описания методов Display соответственно базового и производного классов (листинг 1).


    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string fName, string lName)
        {
            FirstName = fName;
            LastName = lName;
        }

        virtual public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName);
        }
    }
    class Employee : Person
    {
        public string Company { get; set; }

        public Employee(string fName, string lName, string comp)
            : base(fName, lName)
        {
            Company = comp;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + " Место работы: " + Company);
        }
    }
    class Client : Person
    {
        public string Bank { get; set; }

        public Client(string fName, string lName, string bank)
            : base(fName, lName)
        {
            Bank = bank;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + " Банк: " + Bank);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Person[] m = new Person[3];
            m[0] = new Person("Иван", "Петров");
            m[1] = new Client("Семён", "Иванов","Сбербанк");
            m[2] = new Employee("Степан", "Кузнецов", "2ГИС");
            foreach (Person p in m) p.Display();
        }
    }
    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string fName, string lName)
        {
            FirstName = fName;
            LastName = lName;
        }

        virtual public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName);
        }
    }
    class Employee : Person
    {
        public string Company { get; set; }

        public Employee(string fName, string lName, string comp)
            : base(fName, lName)
        {
            Company = comp;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + " Место работы: " + Company);
        }
    }
    class Client : Person
    {
        public string Bank { get; set; }

        public Client(string fName, string lName, string bank)
            : base(fName, lName)
        {
            Bank = bank;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + " Банк: " + Bank);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Person[] m = new Person[3];
            m[0] = new Person("Иван", "Петров");
            m[1] = new Client("Семён", "Иванов","Сбербанк");
            m[2] = new Employee("Степан", "Кузнецов", "2ГИС");
            foreach (Person p in m) p.Display();
        }
    }


Листинг 1. Виртуальные методы


Результат работы программы:


Теперь в цикле foreach вызывается метод Display, соответствующий типу объекта, помещенного в массив.

Виртуальные методы базового класса определяют интерфейс всей иерархии. Этот интерфейс может расширяться в потомках за счет добавления новых виртуальных методов. Переопределять виртуальный метод в каждом из потомков не обязательно: если он выполняет устраивающие потомка действия, метод наследуется.

С помощью виртуальных методов реализуется один из основных принципов объектно-ориентированного программирования — полиморфизм. Это слово в переводе с греческого означает "много форм", что в данном случае означает "один вызов — много методов".

Виртуальные методы незаменимы и при передаче объектов в методы в качестве параметров. В параметрах метода описывается объект базового типа, а при вызове в него передается объект производного класса. Виртуальные методы, вызываемые для объекта из метода, будут соответствовать типу аргумента, а не параметра.


Абстрактные классы

При создании иерархии объектов для исключения повторяющегося кода часто бывает логично выделить их общие свойства в один родительский класс. При этом может оказаться, что создавать экземпляры такого класса не имеет смысла, потому что никакие реальные объекты им не соответствуют. Такие классы называют абстрактными.

Абстрактный класс служит только для порождения потомков. Как правило, в нем задается набор методов, которые каждый из потомков будет реализовывать по-своему. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах.

Абстрактный класс задает интерфейс для всей иерархии, при этом методам класса может не соответствовать никаких конкретных действий. В этом случае методы имеют пустое тело и объявляются со спецификатором abstract.

Абстрактный метод создается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в базовом классе. Это означает, что он должен быть переопределен в производном классе, поскольку его вариант из базового класса просто непригоден для использования. Нетрудно догадаться, что абстрактный метод автоматически становится виртуальным и не требует указания модификатора virtual. В действительности совместное использование модификаторов virtual и abstract считается ошибкой. Для определения абстрактного метода служит приведенная ниже общая форма:

abstract тип имя(список_параметров);

Как видите, у абстрактного метода отсутствует тело. Модификатор abstract может применяться только в методах экземпляра, но не в статических методах (static). Абстрактными могут быть также индексаторы и свойства.

Если в классе есть хотя бы один абстрактный метод, весь класс также должен быть описан как абстрактный, например:


    abstract class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string fName, string lName)
        {
            FirstName = fName;
            LastName = lName;
        }

        abstract public void Display();
    }
    class Employee : Person
    {
        public string Company { get; set; }

        public Employee(string fName, string lName, string comp)
            : base(fName, lName)
        {
            Company = comp;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + "\tМесто работы: " + Company);
        }
    }
    class Client : Person
    {
        public string Bank { get; set; }

        public Client(string fName, string lName, string bank)
            : base(fName, lName)
        {
            Bank = bank;
        }

        override public void Display()
        {
            Console.WriteLine(FirstName + " " + LastName + "\tБанк: " + Bank);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person[] m = new Person[3];
            m[0] = new Client("Иван", "Петров","ВТБ");
            m[1] = new Client("Семён", "Иванов", "Сбербанк");
            m[2] = new Employee("Степан", "Кузнецов", "2ГИС");
            foreach (Person p in m) p.Display();
        }
    }

Результат работы программы:



Абстрактные классы используются при работе со структурами данных, предназначенными для хранения объектов одной иерархии, и в качестве параметров методов.


Бесплодные классы

В C# есть ключевое слово sealed, позволяющее описать класс, от которого, в противоположность абстрактному, наследовать запрещается:

sealed class Client
{
...
} // class Vacya : Client { ... } ошибка!

Большинство встроенных типов данных описано как sealed. Если необходимо использовать функциональность бесплодного класса, применяется не наследование, а вложение, или включение: в классе описывается поле соответствующего типа.

Вложение классов, когда один класс включает в себя поля, являющиеся классами, является альтернативой наследованию при проектировании. Например, если есть объект "двигатель", а требуется описать объект "самолет", логично сделать двигатель полем этого объекта, а не его предком.


Виды взаимоотношений между классами

Механизм наследования классов предоставляет программисту богатейшие возможности организации кода и его многократного использования. Выбор наиболее подходящих средств для целей конкретного проекта основывается на знании механизма их работы и взаимодействия.

Класс object

Корневой класс System.Object всей иерархии объектов .NET, называемый в C# object, обеспечивает всех наследников несколькими важными методами. Производные классы могут использовать эти методы непосредственно или переопределять их.

Класс object часто используется и непосредственно при описании типа параметров методов для придания им общности, а также для хранения ссылок на объекты различного типа — таким образом реализуется полиморфизм.

Открытые методы класса System.Object перечислены ниже.

• Метод Equals с одним параметром возвращает значение true, если параметр и вызывающий объект ссылаются на одну и ту же область памяти. Синтаксис:

public virtual bool Equals(object obj);

• Метод Equals с двумя параметрами возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:

public static bool Equals(object ob1, object ob2);

• Метод GetHashCode формирует хэш-код объекта и возвращает число, однозначно идентифицирующее объект. Это число используется в различных структурах и алгоритмах библиотеки. Если переопределяется метод Equals, необходимо перегрузить и метод GetHashCode. Синтаксис:

public virtual int GetHashCode();

• Метод GetType возвращает текущий полиморфный тип объекта, то есть не тип ссылки, а тип объекта, на который она в данный момент указывает. Возвращаемое значение имеет тип Type. Это абстрактный базовый класс иерархии, использующийся для получения информации о типах во время выполнения. Синтаксис:

public Type GetType();

• Метод ReferenceEquals возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:

public static bool ReferenceEquals(object ob1, object ob2);

• Метод ToString по умолчанию возвращает для ссылочных типов полное имя класса в виде строки, а для значимых — значение величины, преобразованное в строку. Этот метод переопределяют для того, чтобы можно было выводить информацию о состоянии объекта. Синтаксис:

public virtual string ToString()

В производных объектах эти методы часто переопределяют. Например, можно переопределить метод Equals для того, чтобы задать собственные критерии сравнения объектов.


Вопросы для самостоятельной работы студента

1. Для чего используется наследование?

2. Опишите синтаксис производного класса. Какие спецификаторы доступа применяются в иерархиях?

3. Как вызвать метод базового класса из производного?

4. Опишите порядок вызова конструкторов базовых классов при работе конструктора производного класса.

5. Какие ключевые слова используется при переопределении методов базового класса в производном?

6. Опишите механизмы раннего и позднего связывания.

7. Опишите процесс вызова виртуального метода. Для чего применяются виртуальные методы?

8. Все ли методы следует описывать как виртуальные?

9. Для чего используются абстрактные классы?

10. Какие методы и операции класса object часто перегружают в его потомках?

11. Назовите альтернативы наследованию классов.

12. Изучите по справочной системе свойства и методы класса object.

13. Изучите разделы стандарта C#, касающиеся наследования. методы классов.